package format

import (
	"bytes"
	"compress/zlib"
	"encoding/binary"
	"fmt"
	"io"
	"os"

	"gitee.com/thubcc/p2pgit/constant"
	"gitee.com/thubcc/p2pgit/types"
	"gitee.com/thubcc/p2pgit/utils"
	"github.com/a4a881d4/gitcrawling/packext"
	"gopkg.in/src-d/go-git.v4/plumbing"
	"gopkg.in/src-d/go-git.v4/plumbing/format/packfile"
)

// Header the header of compressed object
type Header struct {
	types.Header32
	OHeader packfile.ObjectHeader // original object head
	Base    *types.Header32       // header32 of delta object beased
}

// ToByte Header to bin
func (h *Header) ToByte() (raw []byte) {
	if h.OHeader.Type.IsDelta() {
		raw = make([]byte, 5+32)
		raw[0] = 0x10
		h32 := h.Base.ToHash256()
		copy(raw[5:], h32[:])
	} else {
		raw = make([]byte, 5)
	}
	raw[0] |= (byte(h.OHeader.Type) & 0xf)
	binary.BigEndian.PutUint32(raw[1:5], uint32(h.OHeader.Length))
	return
}

// NewHeader generate header from Object entry
func NewHeader(orig, base *packext.ObjEntry) (r *Header) {
	r = &Header{}
	if orig.OHeader.Type.IsDelta() {
		r.Base = types.NewHeader32FromOE(base)
	}
	r.Header32 = *types.NewHeader32FromOE(orig)
	r.OHeader = *orig.OHeader
	return
}
func checkType(t byte) bool {
	f := plumbing.ObjectType(t & 7)
	if t&0x10 == 0 && f.IsDelta() {
		return false
	}
	return true
}

// HeaderFromByte generate header from bin
func (h *Header) HeaderFromByte(raw []byte) (err error) {
	if len(raw) < 5 {
		err = fmt.Errorf("too short")
		return
	}

	if raw[0]&0x10 != 0 {
		if len(raw) < 5+32 {
			err = fmt.Errorf("too short")
			return
		}
		h.OHeader.Type = plumbing.REFDeltaObject
		var h32 types.Hash256
		copy(h32[:], raw[5:5+32])
		h.Base, err = h32.ToHeader32()
		if err == constant.ErrorBadCrc {
			h.Base.HCRC32 = h.Base.CRC()
		}
	} else {
		h.OHeader.Type = plumbing.ObjectType(raw[0] & 7)
	}
	var length uint32
	length = binary.BigEndian.Uint32(raw[1:5])
	h.OHeader.Length = int64(length)
	return
}

// ExtFromFile read header form file
func ExtFromFile(r io.Reader) (h *Header, err error) {
	h = &Header{}
	var h32 *types.Header32
	h32, err = types.NewHeader32FromFile(r)
	if err != nil {
		return
	}
	h.Header32 = *h32
	var buf = make([]byte, 32+5)
	_, err = io.ReadFull(r, buf[:5])
	if err != nil && err != io.EOF {
		return
	}
	if buf[0]&0x10 != 0 {
		_, err = io.ReadFull(r, buf[5:])
	}
	if err != nil && err != io.EOF {
		return
	}
	if !checkType(buf[0]) {
		fmt.Printf("%s %d %x\n", h32.Hash.String(), h32.Size, buf)
	}
	err = h.HeaderFromByte(buf)
	if err != nil {
		return
	}
	return
}

// TravelPack travel with reader
func TravelPack(fr io.ReadSeeker, cb func(o uint32, h *types.Header32, r io.ReadSeeker) error) (err error) {
	var head types.Hash256
	var offset uint32
	seen := make(map[types.Hash256]bool)
	for err == nil {
		_, err = io.ReadFull(fr, head[:])
		if err == io.EOF {
			err = nil
			break
		}
		if err != nil {
			err = fmt.Errorf("Read %v", err)
			break
		}

		h, crcerr := head.ToHeader32()
		if crcerr != nil {
			h.HCRC32 = h.CRC()
			head = h.ToHash256()
			fmt.Printf("%v %x\n", crcerr, h.Hash)
		}
		if _, ok := seen[head]; !ok {
			err = cb(offset, h, fr)
			if err != nil {
				return
			}
			seen[head] = true
		}

		offset += h.Size + 32
		_, err = fr.Seek(int64(offset), 0)
		if err != nil {
			return
		}
	}
	return
}

// TravelRaw travel with raw
func TravelRaw(fr []byte, cb func(h *types.Header32, r []byte) error) (err error) {
	var head types.Hash256
	for len(fr) > 0 {
		copy(head[:], fr)
		h, crcerr := head.ToHeader32()
		if crcerr != nil {
			h.HCRC32 = h.CRC()
			fmt.Printf("%v %x\n", crcerr, h.Hash)
		}
		size := int(h.Size + 32)
		if size > len(fr) {
			err = fmt.Errorf("out of block")
			fr = []byte{}
			return
		}
		err = cb(h, fr[:size])
		if err != nil {
			return
		}
		fr = fr[size:]
	}
	return
}

// TravelPackFile travel whole pack file, find every object, and apply callback
func TravelPackFile(fn string, cb func(o uint32, h *types.Header32, r io.ReadSeeker) error) (err error) {
	var fr *os.File
	fr, err = os.Open(fn)
	if err != nil {
		return
	}
	defer fr.Close()
	return TravelPack(fr, cb)
}

// Parse the raw body
func Parse(raw []byte) (uncompressed []byte, t plumbing.ObjectType, base *types.Header32, err error) {
	r := bytes.NewReader(raw)
	var h *Header
	h, err = ExtFromFile(r)
	if err != nil {
		return
	}
	if h.OHeader.Type.IsDelta() {
		base = h.Base
	}
	t = h.OHeader.Type
	bufw := new(bytes.Buffer)
	bufw.Reset()
	zr := utils.ZlibReaderPool.Get().(io.ReadCloser)
	defer utils.ZlibReaderPool.Put(zr)

	if err = zr.(zlib.Resetter).Reset(r, nil); err != nil {
		var serr = "no delta"
		if h.OHeader.Type.IsDelta() {
			serr = h.Base.Hash.String()
		}
		err = fmt.Errorf("zlib reset error: %s, %s %s %s", err, h.Header32.Hash.String(), h.OHeader.Type.String(), serr)
		return
	}

	defer zr.Close()
	buf := utils.ByteSlicePool.Get().([]byte)
	_, err = io.CopyBuffer(bufw, zr, buf)
	if err == io.EOF {
		err = nil
	}
	utils.ByteSlicePool.Put(buf)

	uncompressed = bufw.Bytes()
	if len(uncompressed) != int(h.OHeader.Length) {
		err = fmt.Errorf("Wrony size, want %d, but %d", h.OHeader.Length, len(uncompressed))
	}
	return
}

func init() {
	types.RawParse = Parse
}

// ToMap conver header to http header map
func (h *Header) ToMap() (m map[string]string) {
	m = make(map[string]string)
	m["Hash"] = fmt.Sprintf("%0x", h.Header32.ToHash256())
	if h.OHeader.Type.IsDelta() {
		m["Base"] = fmt.Sprintf("%0x", h.Base.ToHash256())
	}
	m["Type"] = h.OHeader.Type.String()
	m["Length"] = fmt.Sprintf("%d", h.OHeader.Length)
	return
}

// ByteLen header conver to bytes length
func (h *Header) ByteLen() int {
	if h.OHeader.Type.IsDelta() {
		return 32 + 5 + 32
	} else {
		return 32 + 5
	}
}
